﻿using System;
using System.Collections.Generic;
using System.Globalization;
using DME.Json.Utilities;

namespace DME.Json.Linq
{
    internal class JPath
    {
        private readonly string _expression;
        public List<object> Parts { get; private set; }

        private int _currentIndex;

        public JPath(string expression)
        {
            ValidationUtils.ArgumentNotNull(expression, "expression");
            _expression = expression;
            Parts = new List<object>();

            ParseMain();
        }

        private void ParseMain()
        {
            int currentPartStartIndex = _currentIndex;
            bool followingIndexer = false;

            while (_currentIndex < _expression.Length)
            {
                char currentChar = _expression[_currentIndex];

                switch (currentChar)
                {
                    case '[':
                    case '(':
                        if (_currentIndex > currentPartStartIndex)
                        {
                            string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
                            Parts.Add(member);
                        }

                        ParseIndexer(currentChar);
                        currentPartStartIndex = _currentIndex + 1;
                        followingIndexer = true;
                        break;
                    case ']':
                    case ')':
                        throw new Exception("Unexpected character while parsing path: " + currentChar);
                    case '.':
                        if (_currentIndex > currentPartStartIndex)
                        {
                            string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
                            Parts.Add(member);
                        }
                        currentPartStartIndex = _currentIndex + 1;
                        followingIndexer = false;
                        break;
                    default:
                        if (followingIndexer)
                            throw new Exception("Unexpected character following indexer: " + currentChar);
                        break;
                }

                _currentIndex++;
            }

            if (_currentIndex - 1 > currentPartStartIndex)
            {
                string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
                Parts.Add(member);
            }
        }

        private void ParseIndexer(char indexerOpenChar)
        {
            _currentIndex++;

            char indexerCloseChar = (indexerOpenChar == '[') ? ']' : ')';
            int indexerStart = _currentIndex;
            int indexerLength = 0;
            bool indexerClosed = false;

            while (_currentIndex < _expression.Length)
            {
                char currentCharacter = _expression[_currentIndex];
                if (char.IsDigit(currentCharacter))
                {
                    indexerLength++;
                }
                else if (currentCharacter == indexerCloseChar)
                {
                    indexerClosed = true;
                    break;
                }
                else
                {
                    throw new Exception("Unexpected character while parsing path indexer: " + currentCharacter);
                }

                _currentIndex++;
            }

            if (!indexerClosed)
                throw new Exception("Path ended with open indexer. Expected " + indexerCloseChar);

            if (indexerLength == 0)
                throw new Exception("Empty path indexer.");

            string indexer = _expression.Substring(indexerStart, indexerLength);
            Parts.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture));
        }

        internal JToken Evaluate(JToken root, bool errorWhenNoMatch)
        {
            JToken current = root;

            foreach (object part in Parts)
            {
                string propertyName = part as string;
                if (propertyName != null)
                {
                    JObject o = current as JObject;
                    if (o != null)
                    {
                        current = o[propertyName];

                        if (current == null && errorWhenNoMatch)
                            throw new Exception("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, propertyName));
                    }
                    else
                    {
                        if (errorWhenNoMatch)
                            throw new Exception("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, propertyName, current.GetType().Name));

                        return null;
                    }
                }
                else
                {
                    int index = (int)part;

                    JArray a = current as JArray;

                    if (a != null)
                    {
                        if (a.Count <= index)
                        {
                            if (errorWhenNoMatch)
                                throw new IndexOutOfRangeException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index));

                            return null;
                        }

                        current = a[index];
                    }
                    else
                    {
                        if (errorWhenNoMatch)
                            throw new Exception("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, current.GetType().Name));

                        return null;
                    }
                }
            }

            return current;
        }
    }
}